/*-------------------------------------------------------------------------
 *
 * heaptoast.c
 *	  变量大小属性的外部和压缩存储的堆特定定义。
 *
 * Copyright (c) 2000-2022, PostgreSQL Global Development Group
 *
 *
 * IDENTIFICATION
 *	  src/backend/access/heap/heaptoast.c
 *
 *
 * INTERFACE ROUTINES
 *		heap_toast_insert_or_update -
 *			尝试通过压缩或移除属性使给定的元组适合一个页面
 *
 *		heap_toast_delete -
 *			当元组被删除时回收toast存储
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/detoast.h"
#include "access/genam.h"
#include "access/heapam.h"
#include "access/heaptoast.h"
#include "access/toast_helper.h"
#include "access/toast_internals.h"
#include "utils/fmgroids.h"


/* ----------
 * heap_toast_delete -
 *
 *	在删除时级联删除 toast 条目
 * ----------
 */
void heap_toast_delete(Relation fc_rel, HeapTuple fc_oldtup, bool fc_is_speculative)
{
	TupleDesc	fc_tupleDesc;
	Datum		fc_toast_values[MaxHeapAttributeNumber];
	bool		fc_toast_isnull[MaxHeapAttributeNumber];

	/*
	 * 我们应该只为普通关系或物化视图的元组调用这个函数 --- 对 toast 关系进行递归调用是糟糕的新闻。
	 */
	Assert(fc_rel->rd_rel->relkind == RELKIND_RELATION ||
		   fc_rel->rd_rel->relkind == RELKIND_MATVIEW);

	/*
	 * 获取元组描述符并将元组分解为字段。
	 *
	 * 注意：在这里使用 heap_deform_tuple() 还是仅使用 heap_getattr() 只获取 varlena 列是有争议的。
	 * 如果 varlena 列较少而非 varlena 列较多，后者可能更胜一筹。然而，
	 * heap_deform_tuple 的时间复杂度仅为 O(N)，而如果有很多 varlena 列，heap_getattr 方法的复杂度将是 O(N^2)，因此从线性成本的角度出发选择前者似乎更合理。（顺便提一下，除非至少有一列 varlena，我们不会到这里。）
	 */
	fc_tupleDesc = fc_rel->rd_att;

	Assert(fc_tupleDesc->natts <= MaxHeapAttributeNumber);
	heap_deform_tuple(fc_oldtup, fc_tupleDesc, fc_toast_values, fc_toast_isnull);

	/* 执行实际工作。 */
	toast_delete_external(fc_rel, fc_toast_values, fc_toast_isnull, fc_is_speculative);
}


/* ----------
 * heap_toast_insert_or_update -
 *
 *	删除不再使用的 toast 条目并创建新的条目以
 *	使新元组可以被插入或更新
 *
 * 输入：
 *	newtup: 要插入的候选新元组
 *	oldtup: 更新时的旧行版本，插入时为 NULL
 *	options: 要传递给 heap_insert() 的 toast 行选项
 * 结果：
 *	如果不需要 toast，则返回 newtup，或者一个 palloc 的修改后的元组
 *	这是实际应被存储的内容
 *
 * 注意：newtup 和 oldtup 都不会被修改。这与该例程的 8.1 之前的 API 有所不同。
 * ----------
 */
HeapTuple heap_toast_insert_or_update(Relation fc_rel, HeapTuple fc_newtup, HeapTuple fc_oldtup,
							int fc_options)
{
	HeapTuple	fc_result_tuple;
	TupleDesc	fc_tupleDesc;
	int			fc_numAttrs;

	Size		fc_maxDataLen;
	Size		fc_hoff;

	bool		fc_toast_isnull[MaxHeapAttributeNumber];
	bool		fc_toast_oldisnull[MaxHeapAttributeNumber];
	Datum		fc_toast_values[MaxHeapAttributeNumber];
	Datum		fc_toast_oldvalues[MaxHeapAttributeNumber];
	ToastAttrInfo fc_toast_attr[MaxHeapAttributeNumber];
	ToastTupleContext fc_ttc;

	/*
	 * 忽略 INSERT_SPECULATIVE 选项。投机插入/超级删除仅通常插入/删除 toast 值。在这里处理似乎是最简单的，而不是在潜在的多个调用者之间处理。
	 */
	fc_options &= ~HEAP_INSERT_SPECULATIVE;

	/*
	 * 我们应该只为普通关系或物化视图的元组调用这个函数 --- 对 toast 关系进行递归调用是糟糕的新闻。
	 */
	Assert(fc_rel->rd_rel->relkind == RELKIND_RELATION ||
		   fc_rel->rd_rel->relkind == RELKIND_MATVIEW);

	/*
	 * 获取元组描述符并将元组分解为字段。
	 */
	fc_tupleDesc = fc_rel->rd_att;
	fc_numAttrs = fc_tupleDesc->natts;

	Assert(fc_numAttrs <= MaxHeapAttributeNumber);
	heap_deform_tuple(fc_newtup, fc_tupleDesc, fc_toast_values, fc_toast_isnull);
	if (fc_oldtup != NULL)
		heap_deform_tuple(fc_oldtup, fc_tupleDesc, fc_toast_oldvalues, fc_toast_oldisnull);

	/* ----------
	 * 准备进行 toast
	 * ----------
	 */
	fc_ttc.ttc_rel = fc_rel;
	fc_ttc.ttc_values = fc_toast_values;
	fc_ttc.ttc_isnull = fc_toast_isnull;
	if (fc_oldtup == NULL)
	{
		fc_ttc.ttc_oldvalues = NULL;
		fc_ttc.ttc_oldisnull = NULL;
	}
	else
	{
		fc_ttc.ttc_oldvalues = fc_toast_oldvalues;
		fc_ttc.ttc_oldisnull = fc_toast_oldisnull;
	}
	fc_ttc.ttc_attr = fc_toast_attr;
	toast_tuple_init(&fc_ttc);

	/* ----------
	 * 压缩和/或保存外部，直到数据适合目标长度
	 *
	 *	1: 直接压缩 attstorage 为 EXTENDED 的属性，并立即存储非常
	 *	   大的属性为 attstorage EXTENDED 或 EXTERNAL 外部
	 *	2: 存储 attstorage 为 EXTENDED 或 EXTERNAL 外部的属性
	 *	3: 直接压缩 attstorage 为 MAIN 的属性
	 *	4: 存储 attstorage 为 MAIN 外部的属性
	 * ----------
	 */

	/* 计算头部开销 --- 这应该与 heap_form_tuple() 匹配 */
	fc_hoff = SizeofHeapTupleHeader;
	if ((fc_ttc.ttc_flags & TOAST_HAS_NULLS) != 0)
		fc_hoff += BITMAPLEN(fc_numAttrs);
	fc_hoff = MAXALIGN(fc_hoff);
	/* 现在转换为元组数据大小的限制 */
	fc_maxDataLen = RelationGetToastTupleTarget(fc_rel, TOAST_TUPLE_TARGET) - fc_hoff;

	/*
	 * 查找 attstorage 为 EXTENDED 的属性进行压缩。还要查找
	 * attstorage 为 EXTENDED 或 EXTERNAL 的大属性，并将它们存储为外部。
	 */
	while (heap_compute_data_size(fc_tupleDesc,
								  fc_toast_values, fc_toast_isnull) > fc_maxDataLen)
	{
		int			fc_biggest_attno;

		fc_biggest_attno = toast_tuple_find_biggest_attribute(&fc_ttc, true, false);
		if (fc_biggest_attno < 0)
			break;

		/*
		 * 尝试在线压缩，如果它具有 attstorage 为 EXTENDED
		 */
		if (TupleDescAttr(fc_tupleDesc, fc_biggest_attno)->attstorage == TYPSTORAGE_EXTENDED)
			toast_tuple_try_compression(&fc_ttc, fc_biggest_attno);
		else
		{
			/*
			 * 具有 attstorage 为 EXTERNAL，忽略后续的压缩
			 * 处理
			 */
			fc_toast_attr[fc_biggest_attno].tai_colflags |= TOASTCOL_INCOMPRESSIBLE;
		}

		/*
		 * 如果这个值本身超过了 maxDataLen（压缩后如有），尽可能立即将其推送到 toast 表中。 
		 * 这样可以避免在常见情况下压缩其他字段，即我们有一个长字段和几个短字段的情况。
		 *
		 * XXX 也许阈值应该小于 maxDataLen？
		 */
		if (fc_toast_attr[fc_biggest_attno].tai_size > fc_maxDataLen &&
			fc_rel->rd_rel->reltoastrelid != InvalidOid)
			toast_tuple_externalize(&fc_ttc, fc_biggest_attno, fc_options);
	}

	/*
	 * 其次，我们寻找存储类型为 EXTENDED 或 EXTERNAL 的属性，这些属性仍然是内联的，并使其成为外部属性。 
	 * 但如果没有 toast 表可供推送，则跳过此步骤。
	 */
	while (heap_compute_data_size(fc_tupleDesc,
								  fc_toast_values, fc_toast_isnull) > fc_maxDataLen &&
		   fc_rel->rd_rel->reltoastrelid != InvalidOid)
	{
		int			fc_biggest_attno;

		fc_biggest_attno = toast_tuple_find_biggest_attribute(&fc_ttc, false, false);
		if (fc_biggest_attno < 0)
			break;
		toast_tuple_externalize(&fc_ttc, fc_biggest_attno, fc_options);
	}

	/*
	 * 第三轮 - 此时我们将存储类型为 MAIN 的属性纳入压缩。
	 */
	while (heap_compute_data_size(fc_tupleDesc,
								  fc_toast_values, fc_toast_isnull) > fc_maxDataLen)
	{
		int			fc_biggest_attno;

		fc_biggest_attno = toast_tuple_find_biggest_attribute(&fc_ttc, true, true);
		if (fc_biggest_attno < 0)
			break;

		toast_tuple_try_compression(&fc_ttc, fc_biggest_attno);
	}

	/*
	 * 最后，我们将类型为 MAIN 的属性外部存储。此时我们增加目标元组大小，
	 * 以便 MAIN 属性除非真的必要，否则不会被外部存储。
	 */
	fc_maxDataLen = TOAST_TUPLE_TARGET_MAIN - fc_hoff;

	while (heap_compute_data_size(fc_tupleDesc,
								  fc_toast_values, fc_toast_isnull) > fc_maxDataLen &&
		   fc_rel->rd_rel->reltoastrelid != InvalidOid)
	{
		int			fc_biggest_attno;

		fc_biggest_attno = toast_tuple_find_biggest_attribute(&fc_ttc, false, true);
		if (fc_biggest_attno < 0)
			break;

		toast_tuple_externalize(&fc_ttc, fc_biggest_attno, fc_options);
	}

	/*
	 * 如果我们将任何值进行了 toast 处理，我们需要使用更改后的值构建一个新的堆元组。
	 */
	if ((fc_ttc.ttc_flags & TOAST_NEEDS_CHANGE) != 0)
	{
		HeapTupleHeader fc_olddata = fc_newtup->t_data;
		HeapTupleHeader fc_new_data;
		int32		fc_new_header_len;
		int32		fc_new_data_len;
		int32		fc_new_tuple_len;

		/*
		 * 计算元组的新大小。
		 *
		 * 注意：我们之前假设旧元组的 t_hoff 必须等于 new_header_len 值，但这并不正确。 
		 * 如果自存储以来有 ALTER TABLE ADD COLUMN 操作，旧元组可能有比当前 natts 更小的值；这将导致对空位图大小的不同结论，
		 * 或者甚至是否需要一个。
		 */
		fc_new_header_len = SizeofHeapTupleHeader;
		if ((fc_ttc.ttc_flags & TOAST_HAS_NULLS) != 0)
			fc_new_header_len += BITMAPLEN(fc_numAttrs);
		fc_new_header_len = MAXALIGN(fc_new_header_len);
		fc_new_data_len = heap_compute_data_size(fc_tupleDesc,
											  fc_toast_values, fc_toast_isnull);
		fc_new_tuple_len = fc_new_header_len + fc_new_data_len;

		/*
		 * 分配并清零所需的空间，并填充 HeapTupleData 字段。
		 */
		fc_result_tuple = (HeapTuple) palloc0(HEAPTUPLESIZE + fc_new_tuple_len);
		fc_result_tuple->t_len = fc_new_tuple_len;
		fc_result_tuple->t_self = fc_newtup->t_self;
		fc_result_tuple->t_tableOid = fc_newtup->t_tableOid;
		fc_new_data = (HeapTupleHeader) ((char *) fc_result_tuple + HEAPTUPLESIZE);
		fc_result_tuple->t_data = fc_new_data;

		/*
		 * 复制现有元组头，但调整 natts 和 t_hoff。
		 */
		memcpy(fc_new_data, fc_olddata, SizeofHeapTupleHeader);
		HeapTupleHeaderSetNatts(fc_new_data, fc_numAttrs);
		fc_new_data->t_hoff = fc_new_header_len;

		/* 复制数据，并在需要时填充空位图 */
		heap_fill_tuple(fc_tupleDesc,
						fc_toast_values,
						fc_toast_isnull,
						(char *) fc_new_data + fc_new_header_len,
						fc_new_data_len,
						&(fc_new_data->t_infomask),
						((fc_ttc.ttc_flags & TOAST_HAS_NULLS) != 0) ?
						fc_new_data->t_bits : NULL);
	}
	else
		fc_result_tuple = fc_newtup;

	toast_tuple_cleanup(&fc_ttc);

	return fc_result_tuple;
}


/* ----------
 * toast_flatten_tuple -
 *
 *	"扁平化" 元组，以包含没有外部 toast 字段。 
 *	（这不会消除压缩或短头数据。）
 *
 *	注意：我们期望调用者已经检查了 HeapTupleHasExternal(tup)，
 *	因此无需短路路径。
 * ----------
 */
HeapTuple toast_flatten_tuple(HeapTuple fc_tup, TupleDesc fc_tupleDesc)
{
	HeapTuple	fc_new_tuple;
	int			fc_numAttrs = fc_tupleDesc->natts;
	int			fc_i;
	Datum		fc_toast_values[MaxTupleAttributeNumber];
	bool		fc_toast_isnull[MaxTupleAttributeNumber];
	bool		fc_toast_free[MaxTupleAttributeNumber];

	/*
	 * 将元组分解为字段。
	 */
	Assert(fc_numAttrs <= MaxTupleAttributeNumber);
	heap_deform_tuple(fc_tup, fc_tupleDesc, fc_toast_values, fc_toast_isnull);

	memset(fc_toast_free, 0, fc_numAttrs * sizeof(bool));

	for (fc_i = 0; fc_i < fc_numAttrs; fc_i++)
	{
		/*
		 * 查看非空的 varlena 属性
		 */
		if (!fc_toast_isnull[fc_i] && TupleDescAttr(fc_tupleDesc, fc_i)->attlen == -1)
		{
			struct varlena *fc_new_value;

			fc_new_value = (struct varlena *) DatumGetPointer(fc_toast_values[fc_i]);
			if (VARATT_IS_EXTERNAL(fc_new_value))
			{
				fc_new_value = detoast_external_attr(fc_new_value);
				fc_toast_values[fc_i] = PointerGetDatum(fc_new_value);
				fc_toast_free[fc_i] = true;
			}
		}
	}

	/*
	 * 形成重新配置的元组。
	 */
	fc_new_tuple = heap_form_tuple(fc_tupleDesc, fc_toast_values, fc_toast_isnull);

	/*
	 * 确保复制元组的身份字段。我们还特别复制可见性信息，以防有人查看 syscache 条目中的这些字段。
	 */
	fc_new_tuple->t_self = fc_tup->t_self;
	fc_new_tuple->t_tableOid = fc_tup->t_tableOid;

	fc_new_tuple->t_data->t_choice = fc_tup->t_data->t_choice;
	fc_new_tuple->t_data->t_ctid = fc_tup->t_data->t_ctid;
	fc_new_tuple->t_data->t_infomask &= ~HEAP_XACT_MASK;
	fc_new_tuple->t_data->t_infomask |=
		fc_tup->t_data->t_infomask & HEAP_XACT_MASK;
	fc_new_tuple->t_data->t_infomask2 &= ~HEAP2_XACT_MASK;
	fc_new_tuple->t_data->t_infomask2 |=
		fc_tup->t_data->t_infomask2 & HEAP2_XACT_MASK;

	/*
	 * 释放分配的临时值
	 */
	for (fc_i = 0; fc_i < fc_numAttrs; fc_i++)
		if (fc_toast_free[fc_i])
			pfree(DatumGetPointer(fc_toast_values[fc_i]));

	return fc_new_tuple;
}


/* ----------
 * toast_flatten_tuple_to_datum -
 *
 *	将包含不在行上的 toasted 字段的元组“扁平化”为 Datum。
 *	结果总是在当前内存上下文中分配。
 *
 *	我们的一个基本规则是，容器类型（行、数组、范围等）的 Datums
 *	不得包含任何外部 TOAST 指针。没有这个规则，我们在准备存储元组时
 *	必须检查每个 Datum，这将是昂贵的，并且无法干净地扩展到新类型的
 *	容器。
 *
 *	然而，我们不想说作为 HeapTuples 表示的元组不能包含 toasted 字段，
 *	因此此例程应在将 HeapTuple 转换为 Datum 时被调用。
 *
 *	顺便说一下，我们还解压任何压缩字段。这对于正确性并不是必需的，
 *	但是反映了一种期望，即如果应用于整个元组而不是单个字段，
 *	压缩将更有效。然而，我们并不关心到要拆解和重建元组只是为了去掉
 *	压缩字段。因此，调用者通常只有在看到元组至少有一个外部字段时
 *	才会调用此例程。
 *
 *	另一方面，在线短头 varlena 字段不受影响。
 *	如果我们在这里“取消 toasted”，它们在 heap_fill_tuple 中
 *	也会被重新转换回短头格式。
 * ----------
 */
Datum toast_flatten_tuple_to_datum(HeapTupleHeader fc_tup,
							 uint32 fc_tup_len,
							 TupleDesc fc_tupleDesc)
{
	HeapTupleHeader fc_new_data;
	int32		fc_new_header_len;
	int32		fc_new_data_len;
	int32		fc_new_tuple_len;
	HeapTupleData fc_tmptup;
	int			fc_numAttrs = fc_tupleDesc->natts;
	int			fc_i;
	bool		fc_has_nulls = false;
	Datum		fc_toast_values[MaxTupleAttributeNumber];
	bool		fc_toast_isnull[MaxTupleAttributeNumber];
	bool		fc_toast_free[MaxTupleAttributeNumber];

	/* 构建一个临时 HeapTuple 控制结构 */
	fc_tmptup.t_len = fc_tup_len;
	ItemPointerSetInvalid(&(fc_tmptup.t_self));
	fc_tmptup.t_tableOid = InvalidOid;
	fc_tmptup.t_data = fc_tup;

	/*
	 * 将元组分解为字段。
	 */
	Assert(fc_numAttrs <= MaxTupleAttributeNumber);
	heap_deform_tuple(&fc_tmptup, fc_tupleDesc, fc_toast_values, fc_toast_isnull);

	memset(fc_toast_free, 0, fc_numAttrs * sizeof(bool));

	for (fc_i = 0; fc_i < fc_numAttrs; fc_i++)
	{
		/*
		 * 查看非空的 varlena 属性
		 */
		if (fc_toast_isnull[fc_i])
			fc_has_nulls = true;
		else if (TupleDescAttr(fc_tupleDesc, fc_i)->attlen == -1)
		{
			struct varlena *fc_new_value;

			fc_new_value = (struct varlena *) DatumGetPointer(fc_toast_values[fc_i]);
			if (VARATT_IS_EXTERNAL(fc_new_value) ||
				VARATT_IS_COMPRESSED(fc_new_value))
			{
				fc_new_value = detoast_attr(fc_new_value);
				fc_toast_values[fc_i] = PointerGetDatum(fc_new_value);
				fc_toast_free[fc_i] = true;
			}
		}
	}

	/*
	 * 计算元组的新大小。
	 *
	 * 这应该与 heap_toast_insert_or_update 中的重建代码匹配。
	 */
	fc_new_header_len = SizeofHeapTupleHeader;
	if (fc_has_nulls)
		fc_new_header_len += BITMAPLEN(fc_numAttrs);
	fc_new_header_len = MAXALIGN(fc_new_header_len);
	fc_new_data_len = heap_compute_data_size(fc_tupleDesc,
										  fc_toast_values, fc_toast_isnull);
	fc_new_tuple_len = fc_new_header_len + fc_new_data_len;

	fc_new_data = (HeapTupleHeader) palloc0(fc_new_tuple_len);

	/*
	 * 复制现有的元组头，但调整 natts 和 t_hoff。
	 */
	memcpy(fc_new_data, fc_tup, SizeofHeapTupleHeader);
	HeapTupleHeaderSetNatts(fc_new_data, fc_numAttrs);
	fc_new_data->t_hoff = fc_new_header_len;

	/* 正确设置复合 Datum 头字段 */
	HeapTupleHeaderSetDatumLength(fc_new_data, fc_new_tuple_len);
	HeapTupleHeaderSetTypeId(fc_new_data, fc_tupleDesc->tdtypeid);
	HeapTupleHeaderSetTypMod(fc_new_data, fc_tupleDesc->tdtypmod);

	/* 复制数据，并在需要时填充空位图 */
	heap_fill_tuple(fc_tupleDesc,
					fc_toast_values,
					fc_toast_isnull,
					(char *) fc_new_data + fc_new_header_len,
					fc_new_data_len,
					&(fc_new_data->t_infomask),
					fc_has_nulls ? fc_new_data->t_bits : NULL);

	/*
	 * 释放分配的临时值
	 */
	for (fc_i = 0; fc_i < fc_numAttrs; fc_i++)
		if (fc_toast_free[fc_i])
			pfree(DatumGetPointer(fc_toast_values[fc_i]));

	return PointerGetDatum(fc_new_data);
}


/* ----------
 * toast_build_flattened_tuple -
 *
 *	构建一个不包含不在行上的 toasted 字段的元组。
 *	（这并不消除压缩或短头 Datum。）
 *
 *	这基本上就像 heap_form_tuple，只是它会提前展开任何外部数据指针。
 *
 *	目前还不清楚在此过程中是否更好地解压在行内压缩的 Datum。
 *	暂时我们不这样做。
 * ----------
 */
HeapTuple toast_build_flattened_tuple(TupleDesc fc_tupleDesc,
							Datum *fc_values,
							bool *fc_isnull)
{
	HeapTuple	fc_new_tuple;
	int			fc_numAttrs = fc_tupleDesc->natts;
	int			fc_num_to_free;
	int			fc_i;
	Datum		fc_new_values[MaxTupleAttributeNumber];
	Pointer		fc_freeable_values[MaxTupleAttributeNumber];

	/*
	 * 我们可以直接将调用者的 isnull 数组传递给 heap_form_tuple，但
	 * 我们可能需要修改值数组。
	 */
	Assert(fc_numAttrs <= MaxTupleAttributeNumber);
	memcpy(fc_new_values, fc_values, fc_numAttrs * sizeof(Datum));

	fc_num_to_free = 0;
	for (fc_i = 0; fc_i < fc_numAttrs; fc_i++)
	{
		/*
		 * 查看非空的 varlena 属性
		 */
		if (!fc_isnull[fc_i] && TupleDescAttr(fc_tupleDesc, fc_i)->attlen == -1)
		{
			struct varlena *fc_new_value;

			fc_new_value = (struct varlena *) DatumGetPointer(fc_new_values[fc_i]);
			if (VARATT_IS_EXTERNAL(fc_new_value))
			{
				fc_new_value = detoast_external_attr(fc_new_value);
				fc_new_values[fc_i] = PointerGetDatum(fc_new_value);
				fc_freeable_values[fc_num_to_free++] = (Pointer) fc_new_value;
			}
		}
	}

	/*
	 * 形成重新配置的元组。
	 */
	fc_new_tuple = heap_form_tuple(fc_tupleDesc, fc_new_values, fc_isnull);

	/*
	 * 释放分配的临时值
	 */
	for (fc_i = 0; fc_i < fc_num_to_free; fc_i++)
		pfree(fc_freeable_values[fc_i]);

	return fc_new_tuple;
}

/*
 * 从堆表中获取 TOAST 切片。
 *
 * toastrel 是要从中获取块的关系。
 * valueid 标识要从中获取块的 TOAST 值。
 * attrsize 是 TOAST 值的总大小。
 * sliceoffset 是要获取的 TOAST 值内的字节偏移量。
 * slicelength 是要从 TOAST 值中获取的字节数。
 * result 是结果应写入的 varlena。
 */
void heap_fetch_toast_slice(Relation fc_toastrel, Oid fc_valueid, int32 fc_attrsize,
					   int32 fc_sliceoffset, int32 fc_slicelength,
					   struct varlena *fc_result)
{
	Relation   *fc_toastidxs;
	ScanKeyData fc_toastkey[3];
	TupleDesc	fc_toasttupDesc = fc_toastrel->rd_att;
	int			fc_nscankeys;
	SysScanDesc fc_toastscan;
	HeapTuple	fc_ttup;
	int32		fc_expectedchunk;
	int32		fc_totalchunks = ((fc_attrsize - 1) / TOAST_MAX_CHUNK_SIZE) + 1;
	int			fc_startchunk;
	int			fc_endchunk;
	int			fc_num_indexes;
	int			fc_validIndex;
	SnapshotData fc_SnapshotToast;

	/* 查找 TOAST 关系的有效索引 */
	fc_validIndex = toast_open_indexes(fc_toastrel,
									AccessShareLock,
									&fc_toastidxs,
									&fc_num_indexes);

	fc_startchunk = fc_sliceoffset / TOAST_MAX_CHUNK_SIZE;
	fc_endchunk = (fc_sliceoffset + fc_slicelength - 1) / TOAST_MAX_CHUNK_SIZE;
	Assert(fc_endchunk <= fc_totalchunks);

	/* 设置扫描键以从索引中获取。 */
	ScanKeyInit(&fc_toastkey[0],
				(AttrNumber) 1,
				BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(fc_valueid));

	/*
	 * 如果获取所有块，则没有额外条件。否则，使用一个块的
	 * 相等条件，其他情况下使用范围条件。
	 */
	if (fc_startchunk == 0 && fc_endchunk == fc_totalchunks - 1)
		fc_nscankeys = 1;
	else if (fc_startchunk == fc_endchunk)
	{
		ScanKeyInit(&fc_toastkey[1],
					(AttrNumber) 2,
					BTEqualStrategyNumber, F_INT4EQ,
					Int32GetDatum(fc_startchunk));
		fc_nscankeys = 2;
	}
	else
	{
		ScanKeyInit(&fc_toastkey[1],
					(AttrNumber) 2,
					BTGreaterEqualStrategyNumber, F_INT4GE,
					Int32GetDatum(fc_startchunk));
		ScanKeyInit(&fc_toastkey[2],
					(AttrNumber) 2,
					BTLessEqualStrategyNumber, F_INT4LE,
					Int32GetDatum(fc_endchunk));
		fc_nscankeys = 3;
	}

	/* 准备扫描 */
	init_toast_snapshot(&fc_SnapshotToast);
	fc_toastscan = systable_beginscan_ordered(fc_toastrel, fc_toastidxs[fc_validIndex],
										   &fc_SnapshotToast, fc_nscankeys, fc_toastkey);

	/*
	 * 通过索引读取块
	 *
	 * 索引是基于 (valueid, chunkidx)，因此它们将按顺序出现
	 */
	fc_expectedchunk = fc_startchunk;
	while ((fc_ttup = systable_getnext_ordered(fc_toastscan, ForwardScanDirection)) != NULL)
	{
		int32		fc_curchunk;
		Pointer		fc_chunk;
		bool		fc_isnull;
		char	   *fc_chunkdata;
		int32		fc_chunksize;
		int32		fc_expected_size;
		int32		fc_chcpystrt;
		int32		fc_chcpyend;

		/*
		 * 获取一个块，提取序列号和数据
		 */
		fc_curchunk = DatumGetInt32(fastgetattr(fc_ttup, 2, fc_toasttupDesc, &fc_isnull));
		Assert(!fc_isnull);
		fc_chunk = DatumGetPointer(fastgetattr(fc_ttup, 3, fc_toasttupDesc, &fc_isnull));
		Assert(!fc_isnull);
		if (!VARATT_IS_EXTENDED(fc_chunk))
		{
			fc_chunksize = VARSIZE(fc_chunk) - VARHDRSZ;
			fc_chunkdata = VARDATA(fc_chunk);
		}
		else if (VARATT_IS_SHORT(fc_chunk))
		{
			/* 由于 heap_form_tuple 做它的工作而可能发生 */
			fc_chunksize = VARSIZE_SHORT(fc_chunk) - VARHDRSZ_SHORT;
			fc_chunkdata = VARDATA_SHORT(fc_chunk);
		}
		else
		{
			/* 永远不应该发生 */
			elog(ERROR, "found toasted toast chunk for toast value %u in %s",
				 fc_valueid, RelationGetRelationName(fc_toastrel));
			fc_chunksize = 0;		/* 保持编译器安静 */
			fc_chunkdata = NULL;
		}

		/*
		 * 对我们找到的数据进行一些检查
		 */
		if (fc_curchunk != fc_expectedchunk)
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %u in %s",
									 fc_curchunk, fc_expectedchunk, fc_valueid,
									 RelationGetRelationName(fc_toastrel))));
		if (fc_curchunk > fc_endchunk)
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %u in %s",
									 fc_curchunk,
									 fc_startchunk, fc_endchunk, fc_valueid,
									 RelationGetRelationName(fc_toastrel))));
		fc_expected_size = fc_curchunk < fc_totalchunks - 1 ? TOAST_MAX_CHUNK_SIZE
			: fc_attrsize - ((fc_totalchunks - 1) * TOAST_MAX_CHUNK_SIZE);
		if (fc_chunksize != fc_expected_size)
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %u in %s",
									 fc_chunksize, fc_expected_size,
									 fc_curchunk, fc_totalchunks, fc_valueid,
									 RelationGetRelationName(fc_toastrel))));

		/*
		 * 将数据复制到结果中的适当位置
		 */
		fc_chcpystrt = 0;
		fc_chcpyend = fc_chunksize - 1;
		if (fc_curchunk == fc_startchunk)
			fc_chcpystrt = fc_sliceoffset % TOAST_MAX_CHUNK_SIZE;
		if (fc_curchunk == fc_endchunk)
			fc_chcpyend = (fc_sliceoffset + fc_slicelength - 1) % TOAST_MAX_CHUNK_SIZE;

		memcpy(VARDATA(fc_result) +
			   (fc_curchunk * TOAST_MAX_CHUNK_SIZE - fc_sliceoffset) + fc_chcpystrt,
			   fc_chunkdata + fc_chcpystrt,
			   (fc_chcpyend - fc_chcpystrt) + 1);

		fc_expectedchunk++;
	}

	/*
	 * 最终检查我们是否成功获取了数据项
	 */
	if (fc_expectedchunk != (fc_endchunk + 1))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg_internal("missing chunk number %d for toast value %u in %s",
								 fc_expectedchunk, fc_valueid,
								 RelationGetRelationName(fc_toastrel))));

	/* 结束扫描并关闭索引。 */
	systable_endscan_ordered(fc_toastscan);
	toast_close_indexes(fc_toastidxs, fc_num_indexes, AccessShareLock);
}
